- 底层如 db 层出现一个 错误该如何处理, 是直接返回 error record not found 还是包装一层再抛给上层<record not found 不建议包装了,但一定要有 error record not found 错误,不要吞掉错误,若想包装的话,记得实现下unwarp,方便上层拿到根因>
- DAO 层与 business 层错误,每层 error 都需要 wrap 返回吗?如果使用自定义错误码的话,哪一层处理比较合适?最外层统一打 log 的话,如何做到给 log 记录完整的上下文信息,给用户只返回友好的提示信息?pkg/errors 如何结合自定义错误码使用?<dao与biz层建议在无需每层都wrap,通常在有业务逻辑的层wrap,记得实现unwrap方便找根因,即wrap相当于手动把当前层的栈记录进去,目的是通过一个错误能定位到具体是哪里抛出的就,哪里需要关心的,比如说你有两个db类库,那么你需要在调用这两个类库的时候wrap,保证能定位到是哪个db抛出来的错误,上下文的话也在最上层记录,甚至可以分两条日志,一条纯错误,一条纯上下文,同一个请求id做关联,方便选择是否开启记录上下文,给用户返回有好提示,可以在最上层做一个fitter统一屏蔽掉错误细节,只返回友好提示,类似 “请求失败 [req_id]”这样的提示,可以带上请求唯一id,方便用户通过这条日志反馈,追踪当时请求链路。pkg/errors主要是提供友好的堆栈,错误码还需要单独处理>
- 出现错误的时候,每一层都打 log 感觉有点冗余,那么放在哪里打比较好呢?不同的层打出的细节信息不一样,需要如何取舍?
一般线上都不会打太多日志,一般都是上报远程log,加一些metric。 通常情况下,会根据返回的框架错误码,和业务错误定位是那一层服务。 之后根据错误码就能知道是哪层出了问题,在问相关层同事定位。所以约定好框架错误码和业务错误码是很重要的。
- 如何将 dao 层错误,以正确的姿态传递/转换成平台层,接入层,应用层的错误?每一层产生的错误的生命周期是啥?(比如 dao 层错误到哪就不传递了?不同层如何协调错误信息)
每一层都有自身定义的错误吗。根据不同的错误码就能知道是哪一层错误。一般都会返回给个个层
可以了解 : https://github.com/go-kratos/kratos/blob/main/errors/errors.go
- 如果 go func 函数依赖于参数,请问如何处理?<1.如果这里问的是函数调用时传入的参数,参数是值传递的,不会有任何问题;是以引用传递的,需要注意临界变量的问题。2.如果这里问的是没有显式传入,而是访问了上下文中存在的变量,go语言支持闭包,闭包可以自动解决这些变量的依赖问题。毛剑老师在视频回放02:20:10处回答了这个问题>
- 返回包含状态码的 err,是通过包装一个包含 error 和状态码的结构体返回还是说有其他更好的方法?<可以参考cratos中的error结构: https://github.com/go-kratos/kratos/blob/v2/errors/errors.go 毛剑老师在视频回放02:21:20处回答了这个问题>
- 在 go 中,err 是否为类似于一个特殊类的理解?携带有内存堆栈信息,可以用于错误处理?
error在go中一个接口定义,标准库中有个基本的实现也就是errors.New来实例化,只是携带了error_message而已。接口定义在builtin包中 https://github.com/golang/go/blob/master/src/builtin/builtin.go#L260 https://github.com/golang/go/blob/master/src/errors/errors.go
携带有内存堆栈信息,是为了快速定位出现错误的文件位置,方便快速排查问题 ,通过利用这个包 https://github.com/pkg/errors
- 下面这段代码会 panic,为什么?
参考官方的这篇文章 https://golang.org/doc/faq#nil_error
意思是interface实现了两个元素,一个是类型T,一个值V。所以在将nil赋值给*commonError的时候,由于error类型是个interface,那err变量就代表着T为*commonError V为nil的接口值,所以err就不是nil值了,但是可以将var err = fn()这样定义,不要定义为interface类型就好了
package main
// 自定义错误类型
type commonError struct {
message string
}
func (ce commonError) Error() string {
return ce.message
}
func main() {
fn := func() *commonError {
return nil // 这个地方返回的 nil 是带上了*commonError 类型,下面定义的就没。
}
var err error
err = fn()
// 解释这个问题需要了解接口的实现,Go 中接口的实现可以理解为两部分
// 1)类型,实现接口方法的类型,可以用%T 查看
// 2)指针,指向那个类型的一个实例对象地址,可以用%+v 查看
// 将下方三行代码的注释去掉,就可以比较出来了
// var err1 error
// fmt.Printf("%+v %T %+v\n", err, err, err == nil)
// fmt.Printf("%+v %T %+v\n", err1, err1, err1 == nil)
if err != nil {
panic(err)
}
}
- 如果底层里返回 err 同时打了日志,调用方收到 err 又打一样日志,这个是不是应该设计去重?还有如果底层有多个地方返回 err 日志级别能是 Info 可能是 Error 级别,那调用方应该的日志级别应该是什么?
- 日志通常直接打印了,没有什么逻辑,也没法去重,如果报错时进行wrap可以在访问日志里打印日志,就可以看到完整的stack信息
- 如果有错误可以直接打印Error,一些业务关键地方打印Info就可以了
- 如果 DAO 层查询数据为空返回 error,外层是不是还需要通过 sentinel error 来区分空和其他数据库错误?还是不理解为什么查询数据为空时应该返回 error。
- 如果是 (*bool,error)呢?error 代表网络等其他错误, *bool == nil 代表找不到
- 或者是返回(*User, error),然后errors.Is(err, sql.ErrNoRow)即可;
- 包内部函数相互调用传递的错误为什么直接上抛,而不用保存堆栈信息?debug 的时候不看调用的层次吗?
- 在错误 wrap 的地方会保存整个调用栈,而不是单个 stack frame。所以调用者不用记录直接上抛。
- 在使用 errWriter 包装后的 WriteResponse 案例中,虽然返回了 error,但是 io.Writer 中已在本次调用中写入一些内容了,而返回的 error 并不能提示`fmt.Fprintf(ew, "HTTP/1.1 %d %s\r\n……)`写入多少,且调用方拿到 error 后也无法将 io.Writer 恢复到这次出错调用之前的状态,因为已经写入一些不完整的信息,请问此时调用方如何处理?那个 io.Writer 要怎么办?

通常是连接已经关闭了,而还在write数据,这时操作已经无效了,所以不存在不完整的需要处理的情况
- 最近在写些繁琐的代码,所以插个问题:Golang 里没有范型,那用什么技巧减少相同逻辑但类型不同的代码?go generate
<
- 你指的繁杂的代码是哪方面的,得有具体案例才能具体讨论。
- 如果是单纯 “用什么技巧减少相同逻辑但类型不同的代码”,这个在 Go 语言现有支持下是没什么好办法的,因为都一定有缺陷,包括你提到的 go generate,早期泛型就用过 go generate,但也有问题。只能说是省去了你手写的方式,但代码量不见得少。
>
推荐毛老师另一部视频https://ww w.bilib ili.com/video/BV1PJ411d7aA
- 如果service层出现error, 就直接return error 吗 ?假设DAO 层没有错误, 这样service层的报错堆栈就不再保存了吗?
<
- 直接 return err 的情况比较少,因为我们在工程实践中还需要 err 的具体上下文信息,因此你可以尝试使用 pkg/errors 的库。
- dao 层没有错误,service 的调用堆栈依然会保存,这似乎没什么冲突?因为错误是来源于 service 层抛出的。
>
- 项目内的 util 工具库(DDD 中的 infra 层)的代码返回的 error 可以 wrap 后返回吗?util 工具库一般不带业务逻辑,并且是可沉降为组件的。
<
- 基础库一般不会直接 error.wrap,大多由外部业务代码进行 wrap,否则很容易嵌套 wrap,也不合理,这是很难控制的。
>
- 请问在调用 RPC,需要重试的时候,如何处理 Error?纠结的地方在于既需要返回最后一个 Error,又需要打印重试时候遇到的 Error。但是如果判断重试次数来决定是不是需要打印,又感觉不太优雅。
<
- 可以将重试时的错误信息集中存储,最后成功或失败时再一次性写入日志。
>
- 想问下最多可以打印多少堆栈?【泛型草案:https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-type-parameters.md】
<
>
- 系统的 log 包默认是标准错误输出 os.Stderr,问:日志分级别时,是不是 os.Stdout 和 os.Stderr 分开处理,怎么处理比较优雅
stdout 和 stderr 是单独的文件(在 supervisor 之类的管理工具中启动时记得重定向),业务日志应该用第三方的 log 包,例如 zap 或者 zerolog。
- errors.Is 用了反射,性能不会有问题吗?
不能看到反射就觉得是瓶颈,任何一种序列化和反序列化有极大量的反射调用。
- 在 Go1.x 中,考虑兼容性,现阶段,是不是直接考虑使用 pkg/errors 就行了?
对的
- 老师说 kit 库要返回根 error,假如 kit 库用到其他的库 A,那么 kit 库是吞掉 A 的 error,向上抛 k 转换后的 kit 自定义的 error,还是直接返回 A 的根 error 给业务代码?
稍微有点没看懂。。不过基本的错误处理原则是,在你的项目内部,第一次发生错误的时候,
wrap 这个错误,并返回。
- 毛老师,这个 errors 包打印堆栈信息,应该对性能有不小的影响吧,对于高并发的流量很大的接口应该不适用 wrap,在工程实践里面,这个包最终目的主要是把 error 上下文收集起来,集中到日志打印吗,因为完整的 error 肯定是不能直接抛给用户的
在正常的场景下,业务逻辑应该是能正常运行的,不会产生error,所以不用去wrap,所以这个对性能的影响可以忽略,如果你的实际应用场景对性能及其敏感,那么建议还是实际测试一下,用数据说话,另外也可以类比下其他语言,比如C++和java,报错了也是会打印堆栈信息的,方便排错。另外面向用户的错误都必须是用户能理解的,经过加工的,肯定不是把原始错误抛出去,所以不会带着堆栈信息的。BTW: 如果是真的性能敏感的系统,可能连日志都不能打印的。
- 感觉 DAO 如果找不到回 error 还是有点怪,回个 struct 像这样会不会好点?
type struct Option { HasValue bool Data interface{} }
如果回个struct,那么就需要首先定义这个struct,然后再封装这个struct,上层调用到要去判断struct是不是nil,然后再去取值,这样做是不是有点太麻烦了?直接返回个错误判断不是更简单么?
- 根据业务需要对 pkg/error 进行修改封装成自己的类。请问老师现在合理的使用方式是怎样的,1. 直接引用或者借鉴重写;2. 有什么企业项目中好的 error 包,包含对 code 的处理,分享一下。
直接引用还是重写看你项目的大小和业务需要了,
可以参考B站的开源项目https://github.com/go-kratos/kratos/tree/v2
- 1.3 的 Is 和 As 有什么区别, 分别是在什么场景下去使用?
https://blog.golang.org/go1.13-errors
简单来说:the errors.Is
function behaves like a comparison to a sentinel error, and the errors.As
function behaves like a type assertion.
- 分层图解 errors 的处理流程
- 直接在打印堆栈的时候调用堆栈信息不就可以了为什么还要在生成 err 的时候 wrap 呢?debug.PrintStack()可以打印
回答:1. 我们大部分时候不会将error记录系统的堆栈,这个比较消耗性能。err wrap可以理解为是手动堆栈。2. wrap相当于是对error的一个补充,在某些情况下,报error不一定是业务错误,例如数据库里查不到数据也报的error,业务上可能要定位在哪个地方报的查不到,也需要wrap下,而不需要记录所有的系统堆栈信息。
- 毛老师,注意 wrap 使用时机主要为了防止堆栈信息过多,如果我 service 层调用了 dao 层一个 wrap 过 err 的接口, 自身也想带点信息过去方便排查错误,不用 wrap 有什么好方法吗
回答:没太明白意思,我觉得还可以用统一错误码
- 如果在基础库底层方法中使用 warp 包起来,在出口位置,使用 errors.Cause 统一处理一下,然后返回错误,这样处理是不是也可以
回答:有的地方比较适合这种。例如做监控或者记录日志,根因处理错误后,可以收敛错误数量,做统计,加快报警和排查速度。
- 如果需要记录异常日志,应该在哪层处理
一般入口和出口是要记录异常日志的。
一般各层的关注点不同,错误都会打印的,比如方法的重要入参,返回值,具体错误等。
可以用wrap透传各层精简的err信息。
- go 做爬虫好做吗
可以的。爬虫的有几块:调度,状态存储,数据清洗,数据存储等,这一块python的框架丰富些。看团队的技术栈进行选择,用go的话就用go,用go实现这些都不难。
- RPC HTTP 都去调用 services 层,那是分别在 RPC 和 HTTP 层记日志吗?
每层关注点不同,个人认为错误日志可以每层都记录你关注的点,通过id串联
- beego 框架,controller、service、dao 3 层,错误传到 controller 层处理?第一层 wrap 后面都是透传?
可以每层wrap,可以看出来调用栈。
- 基础库如果不 wrap,那自己在开发时,调试会很麻烦,如何处理?
答:你说的不wrap,是不是指,基础库可能把错误用fmt之类的处理过,以至于丢失了原始的信息?在这种情况下,丢失了堆栈信息之后,确实非常难以调试,一般的实践就是二分法打断点下去,直到确认发生错误的准确位置。而如果是指基础库直接把错误返回,没有wrap,那么我们还是能够拿到这个错误的堆栈信息的。
- 假如一次请求操作中返回的错误信息是多条的,这种情况有没有好的处理方式?比如,从 Excel 中导入数据到数据库,每行数据都可能出现校验错误之类的错误信息,这种情况下,有没有比较优的解决之道。
答:这个纯粹看你的场景。第一种情况是,数据必须非常严格被处理掉,意味着,中间任何一条数据出错,后面的都不会被处理,这种情况下,都不需要返回多条错误;如果是尽力模式,也就是问题描述的这种场景,那么就只能遍历结果集,找出处理失败的,首先要将所有的错误详尽的记录下来。这个时候我其实建议返回的结果是一个map,这样在打日志的时候,可以准确记录是哪条数据出了错,比如说<idxxx, errxxx>这样记录。或者返回一个list,里面要么是nil,要么是error。这种返回形式,需要用下标来匹配数据和错误信息。我们不太推荐使用结构体,但是在这种场景之下,返回自定义的错误类型,里面持有具体的数据信息,也是可以接受的。
- 底层包括哪些东西,需要看什么资料和书 (UNP,UAP, Linux, 操作系统)
答:一般提及底层,基本上就是网络、编译原理、操作系统。我个人推荐《深入理解计算机原理》,龙书,《计算机程序的构造与解释》,《现代操作系统》,有时间应该学个汇编,不需要熟练掌握,能够看懂大概就可以了。Golang因为还有GC,所以额外推荐《垃圾回收算法手册》。如果刚离开学校不久,可以考虑重新复习一下大学的基础课程,特别是网络、编译原理和操作系统相关的。我看到下面42题也类似,里面推荐了Go相关的底层,也可以参考。
- 基础库调用另一个基础的时,另一个基础库返回的错误也不需要 wrap 直接返回?
答:如果当前的基础库并没有更多的信息需要wrap在里面,那么直接返回就可以了。
- 搭建框架基础库的,包未发布到 GitHub 上,在在 go mod 下载基础包的时候,无法自动进行下载,这种情况应该怎么处理,go mod 如何搭建私有库 【私有 git 仓库就可以 设置 private,当然也可以再搭个 proxy 做缓存镜像】
答:可以吧pkg放在另外一个仓库,也可以不用go mod,统一放在vendor里
- 我理解数据查询不到返回空是一种业务正常状态,为什么会返回 error 呢。感觉 errors 堆栈没 debug 堆栈直观
答:数据找不到和数据存在但是是空值是两码事,如果都统一反馈空,并不能确定数据到底存不存在
- 下面代码中发生错误后 Write 方法还会被多次执行,代码执行效率与代码简洁怎么取舍


答:代码是写个工程师看的,而不是写给机器看的,所以首先要考虑代码的可读性
40.基础库要定义 sentinal error 吗?
答:这个要根据公司业务和团队,某些公司有专门的监控不需要在lib层做
41.我怎么知道我是不是最顶层?
你肯定不知道你是最顶层,你只能知道你调用了谁,但是不知道谁会调用你。
42底层原理,需要看什么书之类的
其实最好的学习途径就是通过官网:https://golang.google.cn/doc/,其中 https://golang.google.cn/doc/effective_go.html#concurrency 和 https://golang.google.cn/ref/mem 都是其中关键。如果需要中文版,也可以看:https://go-zh.org/doc/ ,如果需要书也可以选择:GO语言高级编程。
43处理一个函数中的多个 error 处理方式是声明一个结构体吗
场景是不是与 34 题一样?如果是的话,可以选择返回 []error 或者 map[id]error。声明一个结构体是用于你想在错误里面携带更多上下文信息的时候使用。
44.func write() error {
buffer, err := openFile("path")
if err != nil {
return err
}
err := bufferTofile(buffer)
if err != nil {
return err
}
err := closeFile()
if err != nil {
return err
}
}这种是声明一个结构体来处理错误吗刚刚听得不是很懂?
如果你需要获取程序上下文信息,如:行号、堆栈等,则需要声明一个结构体处理。
这块 mysql 的查询,要明确的写出每个 SELECT 语句的字段,有什么 简单 的方法 吗?
var x App
var y []App
for rows.Next() {
_ = rows.Scan(&x.a,%x.b)
temp := App{
Name: x.a,
Value: x.b,
}
result = append(result, temp)
}
可以选用orm框架,如:gorm、beego等都有提供类似能力。
45.老师建议公共组件中的 error 直接抛。不过说 wrap 最好在第一次出现时包装。那组件中的 error 不是应该 wrap 吗?
我的理解:如果只是自已使用的,然后第一次出现的错误,那就进行 warp,如果是给别人使用的,就直接抛出底层错误,然后给调用方使用的时候,去决定要不要 warp。不知道这样理解对不对?
46.老师,errors 对象是通过 context 往下传递吗?有什么优雅的写法?另外,例如 biz 层调用 dao 层的包,用的是什么方式?依赖注入?还是全局变量?还是有其他更优雅的写法?
47.老师好,上课举得这个例子,如果直接 return err,不包装也是可以使用 errors.Is 进行判断的吧? 为什么还要包装返回呢? 是强制让调用者使用 errors.Is 来处理吗?是的,包装返回方法的作者就可以随意添加/更改附加的 error 上下文,同时不破坏调用方的判断,不包装调用方可能用==判断,这样方法作者考虑到兼容性不可随意添加 error 上下文

48.毛老师,您刚才不是说在 dao 中在表中没有查询到数据建议 return error 错误,而不是 nil,那么在 service 层怎么去判断,是没有数据还是真正的 error 错误?
49.B 站上传视频速度真快是直接用的公有云,还是自己处理的?
50.老师,一个项目即提供和了 http 服务,又提供了 rpc 服务,如果两个项目都是这样设计就会互相调用合理吗?有啥好的解决方法
51.service 层是最前端,提供 api 的,dao 是数据访问层,那么 biz 具体是做什么的呢?
A:biz 层一般是处理业务逻辑的
52.毛老师,请问下 ppt 里面说的 wrap 是 pkg/errors 中的吗,非标准库,因为和标准包跳来跳去感觉有点晕,因为我看现在的 errors 中也有 wrap 是没有堆栈信息吗